Skip to content

Unify reflection helpers#7344

Open
Evangelink wants to merge 20 commits into
mainfrom
dev/amauryleve/reflection
Open

Unify reflection helpers#7344
Evangelink wants to merge 20 commits into
mainfrom
dev/amauryleve/reflection

Conversation

@Evangelink
Copy link
Copy Markdown
Member

@Evangelink Evangelink commented Feb 4, 2026

Summary

Unify the reflection helper infrastructure by consolidating ReflectHelper logic into ReflectionOperations (the IReflectionOperations implementation). This eliminates a layer of indirection and makes the reflection surface area consistent across the adapter and platform services.

Changes

Consolidation into IReflectionOperations / ReflectionOperations

  • Extended IReflectionOperations with higher-level generic methods previously only available on ReflectHelper:
    • IsAttributeDefined<TAttribute>
    • GetFirstAttributeOrDefault<TAttribute>
    • GetSingleAttributeOrDefault<TAttribute>
    • GetAttributes<TAttributeType>
    • GetCustomAttributesCached
    • IsMethodDeclaredInSameAssemblyAsType
  • Moved the attribute cache (ConcurrentDictionary<ICustomAttributeProvider, Attribute[]>) from ReflectHelper into ReflectionOperations.
  • Made ReflectionOperations inherit from MarshalByRefObject (needed for .NET Framework app-domain isolation).

New helper: ReflectionHelper (extension methods)

  • Created ReflectionHelper in MSTestAdapter.PlatformServices with extension methods on IReflectionOperations for:
    • GetTestCategories — collects TestCategoryBaseAttribute from method, type, and assembly levels.
    • GetTestPropertiesAsTraits — extracts TestPropertyAttribute values as Trait[].

Removed code

  • Deleted ReflectHelper singleton and its per-file usages across the adapter and platform services.
  • Deleted ReflectionOperationsNetFrameworkAttributeHelpers.cs — the .NET Framework-specific reflection-only-load logic is no longer needed.
  • Deleted the ReflectHelperTests unit test file (tests moved to ReflectionOperationsTests).

Call-site migrations

  • Updated all call sites that used ReflectHelper.Instance to go through PlatformServiceProvider.Instance.ReflectionOperations instead:
    • TestAssemblySettingsProvider, TestMethodInfo, TestMethodRunner, AttributeHelpers, TestDeployment, DeploymentItemUtility, DeploymentUtilityBase.

Test infrastructure

  • Introduced MockableReflectionOperations — a wrapper that delegates non-generic IReflectionOperations methods to a Moq mock, and implements the new generic methods by filtering mock results. This solves the problem of Moq not supporting generic methods with type constraints.
  • Updated TestablePlatformServiceProvider.SetupMockReflectionOperations() to use the new wrapper.
  • Migrated and expanded tests from ReflectHelperTests into ReflectionOperationsTests (attribute caching, IsAttributeDefined, GetTestCategories, etc.).
  • Updated ReflectionUtilityTests (integration tests) to use Assembly.LoadFrom instead of the removed Assembly.ReflectionOnlyLoadFrom.
  • Added new test cases for the GetAttributes<T> generic method on both unit and integration test levels.

@Evangelink Evangelink marked this pull request as ready for review February 6, 2026 13:39
@Evangelink Evangelink enabled auto-merge February 6, 2026 13:39
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs Outdated
# Conflicts:
#	src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/ReflectHelperTests.cs
…tion

# Conflicts:
#	src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Utilities/ReflectionOperationsNetFrameworkAttributeHelpers.cs
#	test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/ReflectionUtilityTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/ReflectionUtilityTests.cs
Copy link
Copy Markdown
Member

@Youssef1313 Youssef1313 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we are good now that everything uses the caching ReflectHelper on current main?

(Except that we need to investigate if it's possible to simplify the implementation of reflection operations under .NET Framework)

@Evangelink
Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

…tion

# Conflicts:
#	src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Execution/TypeCache.cs
#	src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TestMethodValidatorTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs
#	test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 17, 2026 18:40
@Evangelink Evangelink review requested due to automatic review settings April 17, 2026 18:40
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 17, 2026

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts and pushed the merge commit: 19c164645.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • k4kvsblobprodcus344.vsblob.vsassets.io
    • Triggering command: /home/REDACTED/work/testfx/testfx/.dotnet/dotnet /home/REDACTED/work/testfx/testfx/.dotnet/dotnet msbuild /m /nologo /clp:Summary /v:minimal /nr:true /warnaserror /p:TreatWarningsAsErrors=true /p:ContinuousIntegrationBuild=false /home/REDACTED/work/testfx/testfx/artifacts/toolset/restore.proj /t:__WriteToolsetLocation /clp:ErrorsOnly;NoSummary /p:__ToolsetLocationOutputFile=/home/REDACTED/work/testfx/testfx/artifacts/toolset/11.0.0-beta.26211.1.txt (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Evangelink
Copy link
Copy Markdown
Member Author

@copilot fix the following issues:

@azure-pipelines
azure-pipelines
/ microsoft.testfx (Build Linux Debug)
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs#L68

src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs(68,36): error CS1503: (NETCORE_ENGINEERING_TELEMETRY=Build) Argument 1: cannot convert from 'Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.IReflectionOperations' to 'Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers.ReflectHelper'
Check failure on line 146 in src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs

@azure-pipelines
azure-pipelines
/ microsoft.testfx (Build Linux Debug)
src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs#L146

src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs(146,47): error CS1503: (NETCORE_ENGINEERING_TELEMETRY=Build) Argument 1: cannot convert from 'Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers.ReflectHelper' to 'Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.IReflectionOperations'

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 21, 2026

@copilot fix the following issues:

@azure-pipelines
...

Fixed and pushed in f6aa41d.

I aligned the affected reflection-related files to consistent types, which resolves the CS1503 constructor/signature mismatches and related compile errors.

UI screenshot: N/A (no UI changes in this PR).

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • c50vsblobprodcus330.vsblob.vsassets.io
    • Triggering command: /home/REDACTED/work/testfx/testfx/.dotnet/dotnet /home/REDACTED/work/testfx/testfx/.dotnet/dotnet msbuild /m /nologo /clp:Summary /v:minimal /nr:true /warnaserror /p:TreatWarningsAsErrors=true /p:ContinuousIntegrationBuild=false /home/REDACTED/.nuget/packages/microsoft.dotnet.arcade.sdk/11.0.0-beta.26211.1/tools/Build.proj /p:Configuration=Debug /p:RepoRoot=/home/REDACTED/work/testfx/testfx/ /p:Restore=true /p:Build=true /p:DotNetBuild=false /p:DotNetBuildSourceOnly=false /p:DotNetBuildFromVMR=false /p:Rebuild=false /p:Test=true (dns block)
  • jd4vsblobprodcus366.vsblob.vsassets.io
    • Triggering command: /home/REDACTED/work/testfx/testfx/.dotnet/dotnet /home/REDACTED/work/testfx/testfx/.dotnet/dotnet msbuild /m /nologo /clp:Summary /v:minimal /nr:true /warnaserror /p:TreatWarningsAsErrors=true /p:ContinuousIntegrationBuild=false /home/REDACTED/.nuget/packages/microsoft.dotnet.arcade.sdk/11.0.0-beta.26211.1/tools/Build.proj /p:Configuration=Debug /p:RepoRoot=/home/REDACTED/work/testfx/testfx/ /p:Restore=true /p:Build=true /p:DotNetBuild=false /p:DotNetBuildSourceOnly=false /p:DotNetBuildFromVMR=false /p:Rebuild=false /p:Test=true (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

# Conflicts:
#	src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs
Copilot AI review requested due to automatic review settings May 11, 2026 10:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses #3043 by consolidating reflection-related behavior around IReflectionOperations/ReflectionOperations, replacing several usages of ReflectHelper in PlatformServices (and updating tests accordingly) while introducing a small ReflectionHelper utility for test metadata extraction.

Changes:

  • Extend IReflectionOperations and ReflectionOperations with cached/generic attribute APIs (e.g., GetAttributes<T>, GetCustomAttributesCached, IsAttributeDefined<T>).
  • Update deployment + execution code paths to use IReflectionOperations instead of ReflectHelper in key locations.
  • Refactor and expand unit/integration tests, adding MockableReflectionOperations to support Moq scenarios.
Show a summary per file
File Description
test/UnitTests/MSTestAdapter.UnitTests/MSTestAdapter.UnitTests.csproj Links shared test helper MockableReflectionOperations into MSTestAdapter.UnitTests.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/ReflectionUtilityTests.cs Updates tests to use ReflectionOperations instance and adds generic attribute tests.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs Switches deployment tests to mock IReflectionOperations instead of ReflectHelper.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs Switches to IReflectionOperations mocks and adjusts helper signatures.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs Wraps mocked reflection ops with MockableReflectionOperations to support generic API usage.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs New test-only wrapper to make IReflectionOperations easier to use with Moq.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs Updates to use IReflectionOperations in deployment tests.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs Moves/rewrites tests previously covering ReflectHelper onto ReflectionOperations.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs Uses IReflectionOperations for deployment item utility in desktop tests.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopReflectionOperationsTests.cs Adjusts assertions to account for compiler-generated attributes.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/ReflectHelperTests.cs Removes obsolete ReflectHelper test suite (migrated to ReflectionOperationsTests).
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/MethodInfoExtensionsTests.cs Updates extension method tests to pass IReflectionOperations.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs Switches cache clearing + trait extraction to ReflectionOperations/TypeCache.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs Updates reflection-related setups to new IReflectionOperations API shape.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/ClassCleanupManagerTests.cs Removes unused ReflectHelper usage (but now has an unused using).
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeValidatorTests.cs Attempts to swap ReflectHelper mocks for IReflectionOperations mocks.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs Attempts to construct discovery components using IReflectionOperations wrappers.
test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/ReflectionUtilityTests.cs Switches from reflection-only loading to Assembly.LoadFrom and adds generic attribute coverage.
src/Adapter/MSTestAdapter.PlatformServices/Utilities/ReflectionOperationsNetFrameworkAttributeHelpers.cs Removes NETFRAMEWORK reflection-only attribute helper implementation.
src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs Updates default ctor to use ReflectionOperations from PlatformServiceProvider.
src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs Changes dependency from ReflectHelper to IReflectionOperations.
src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs Updates default ctor to use ReflectionOperations from PlatformServiceProvider.
src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs Adds caching + generic attribute APIs and removes NETFRAMEWORK special casing.
src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs Expands interface with generic/cached attribute APIs.
src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs Introduces helper extensions for categories and test property traits.
src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs Routes ignore-condition logic via PlatformServiceProvider.Instance.ReflectionOperations.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs Uses ReflectionOperations.GetCustomAttributesCached via platform service provider.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs Routes attribute queries via PlatformServiceProvider.Instance.ReflectionOperations.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblySettingsProvider.cs Replaces static ReflectHelper assembly attribute checks with IReflectionOperations.

Copilot's findings

Comments suppressed due to low confidence (2)

src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs:277

  • GetCustomAttributesNotCached assigns the result of GetCustomAttributes(MemberInfo) (nullable) into a non-nullable object[] local. This produces nullable warnings and can hide an actual null return. Make the local object[]? (or coalesce to Array.Empty()) so the nullability matches the method’s contract.
            public static object[]? GetCustomAttributesNotCached(ICustomAttributeProvider attributeProvider)
            {
                IReflectionOperations reflectionOperations = PlatformServiceProvider.Instance.ReflectionOperations;
                object[] attributesArray = attributeProvider is MemberInfo memberInfo
                    ? reflectionOperations.GetCustomAttributes(memberInfo)
                    : reflectionOperations.GetCustomAttributes((Assembly)attributeProvider, typeof(Attribute));
    
    

    test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeValidatorTests.cs:107

    • Same constructor mismatch here: TypeValidator currently expects a ReflectHelper (and overload with discoverInternals also takes ReflectHelper), but this test passes IReflectionOperations. This should be adjusted to match the production signature (or update production code to use IReflectionOperations).
        public void WhenInternalDiscoveryIsEnabledIsValidTestClassShouldReturnTrueForInternalTestClasses()
        {
            var typeValidator = new TypeValidator(_mockReflectionOperations.Object, true);
    
    
    • Files reviewed: 29/29 changed files
    • Comments generated: 10

Comment thread src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs Outdated
Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx
PR: #7344 — Unify reflection helpers


Key Findings

This PR refactors tests to use the new unified ReflectionOperations/IReflectionOperations API (replacing ReflectHelper). The migration is structurally sound — the deleted ReflectHelperTests.cs tests have been faithfully ported into ReflectionOperationsTests.cs, and the new MockableReflectionOperations helper is well-designed. Tests correctly manage PlatformServiceProvider.Instance state via constructor setup and Dispose.

The following issues are all non-blocking cleanup items introduced during the migration:

  1. [Assertion] 12 occurrences of SequenceEqual(...).Should().BeTrue() — These produce completely opaque failure messages on CI. Use actual.Should().Equal(expected) for sequence-order assertions (shown in inline comment at line 59 of ReflectionOperationsTests.cs).

  2. [Structure] Stale test names HasAttributeDerivedFrom* — These methods now call rh.IsAttributeDefined<T>() internally (the old HasAttributeDerivedFrom method no longer exists). Names should be updated to reflect what's actually being tested.

  3. [Structure] Two dead code comments// new Mock<MemberInfo>(); at lines 258 and 305 should be removed.

  4. [Assertion] Redundant Moq setup in TypeValidatorTests — Two identical It.IsAny<TypeInfo>() setups where the first (Returns(false)) is immediately overridden by the second (Returns(true)). Caused by a matcher change during migration from It.IsAny<Type>().

Recommendations

All issues are minor clean-up items that don't affect test correctness or reliability. No flakiness risks or false-positives were identified. The PR can proceed — the suggestions can be addressed in a follow-up or in this PR at the author's discretion.


Generated by Test Expert Reviewer

🧪 Test quality reviewed by Test Expert Reviewer 🧪

Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Workflow: pr-expert-reviewer 🧠
Date: 2026-05-11
Repository: microsoft/testfx

Key Findings

🔴 Correctness — Unsafe concrete-type casts (2 occurrences)

Both TestDeployment.cs:37 and DeploymentUtilityBase.cs:28 were changed to use the new IReflectionOperations abstraction — but both no-arg constructors still cast through to the concrete type:

new DeploymentItemUtility((ReflectionOperations)PlatformServiceProvider.Instance.ReflectionOperations)

DeploymentItemUtility now accepts IReflectionOperations, making the cast entirely unnecessary. It will throw InvalidCastException any time the active platform-service provider returns something other than the concrete ReflectionOperations (e.g., test scenarios using SetupMockReflectionOperations() / MockableReflectionOperations).

🟡 Correctness — DeclaringType! null-forgiving on a nullable property

IsMethodDeclaredInSameAssemblyAsType uses method.DeclaringType!.Assembly — the null-forgiving operator suppresses the compiler warning but does not prevent a NullReferenceException if a MethodInfo with DeclaringType == null is ever passed. A ?. guard with an appropriate fallback is the safer pattern.

Positive Observations

  • The overall design is good: moving attribute caching directly into ReflectionOperations (away from ReflectHelper) is the right architectural move and significantly reduces duplication.
  • ConcurrentDictionary<ICustomAttributeProvider, Attribute[]> for the attribute cache is thread-safe and well-suited for the parallel test-execution model.
  • MockableReflectionOperations elegantly bridges the Moq generic-method-constraint gap and enables clean test isolation.
  • The GetTestPropertiesAsTraits refactor (two-pass count + single allocation) is a nice performance improvement.
  • Deletion of ReflectionOperationsNetFrameworkAttributeHelpers.cs (reflection-only load path) appears safe: code that still needs reflection-only load (AssemblyLoadWorker) already accesses CustomAttributeData.GetCustomAttributes directly and does not go through ReflectionOperations.

Generated by pr-expert-reviewer 🧠

🧠 Reviewed by Expert Code Reviewer 🧠

Comment thread src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs Outdated
Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx
PR: #7344 — Unify reflection helpers (updated review, head 0253483)


Key Findings

This review covers the new commits since the previous automated review (52b45760253483). The migration from ReflectHelper to IReflectionOperations across the test suite is structurally sound. MockableReflectionOperations is well-designed and correctly bridges the Moq generic-constraint gap. TestablePlatformServiceProvider.SetupMockReflectionOperations wrapping is correct. The new GetTestCategories and IsAttributeDefined tests are well-isolated (state cleaned up via Dispose).

Four issues found — none are flakiness/isolation bugs, all are quality improvements:

  1. [Structure] Dead SetupDeploymentItems method with pragma suppression in DesktopTestDeploymentTests.cs — the method is never called; a valid IDE0051 warning is suppressed rather than the dead code being removed or wired in.

  2. [Assertion] AttributeMockingHelper.SetCustomAttribute ignores its Type parameter — the stored Type is never used in GetCustomAttributesNotCached, making the parameter misleading and creating a latent false-positive risk for future tests.

  3. [Assertion] BeEquivalentTo used for ordered-sequence assertions in ReflectionUtilityTests.cs (lines 49, 63, 129) — where the test clearly expects a specific attribute order (derived before base), order-insensitive comparison masks regressions.

  4. [Assertion] SequenceEqual(...).Should().BeTrue() (12 occurrences) — already flagged in the prior review; still unaddressed. Produces opaque CI failures.

Recommendations

  • Remove or wire in SetupDeploymentItems in DesktopTestDeploymentTests.cs
  • Either apply type-based filtering in GetCustomAttributesNotCached, or drop the Type type parameter from SetCustomAttribute
  • Replace BeEquivalentTo with Equal for tests that encode a meaningful attribute order
  • Replace SequenceEqual(...).Should().BeTrue() with actual.Should().Equal(expected) throughout

Generated by Test Expert Reviewer 🧪

🧪 Test quality reviewed by Test Expert Reviewer 🧪

🧪 Test quality reviewed by Test Expert Reviewer 🧪

Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Workflow: Expert Code Reviewer 🧠
Date: 2026-05-11
Repository: microsoft/testfx


Key Findings

🔴 Correctness — Two surviving concrete-type casts (unresolved from prior review)

TestDeployment.cs:37 and DeploymentUtilityBase.cs:28 both no-arg constructors still contain (ReflectionOperations)PlatformServiceProvider.Instance.ReflectionOperations. Since DeploymentItemUtility was updated to accept IReflectionOperations, the cast is entirely unnecessary — and actively harmful. It will throw InvalidCastException at runtime when the service provider resolves to anything other than the concrete ReflectionOperations (e.g., the MockableReflectionOperations now in the test project). The "Fix build errors" commit (latest, 0253483) did not address these.

🔴 Correctness — DeclaringType! null-forgiving in IsMethodDeclaredInSameAssemblyAsType (unresolved from prior review)

method.DeclaringType!.Assembly.Equals(type.Assembly) at line 287 of ReflectionOperations.cs silences the compiler but does not prevent a NullReferenceException. MethodInfo.DeclaringType is nullable by contract (returns null for global IL methods). A ?. guard returning false is the correct fix.

🟡 Correctness — Unguarded (Assembly) cast in NotCachedReflectionAccessor (new)

NotCachedReflectionAccessor.GetCustomAttributesNotCached (line 279) falls back to (Assembly)attributeProvider when the provider is not a MemberInfo. ICustomAttributeProvider is also implemented by Module and ParameterInfo — passing either silently throws InvalidCastException. A switch expression covering all known cases with a descriptive exception for the rest would prevent silent surprises.


Positive Observations

  • Moving the attribute cache from the old ReflectHelper static singleton into ReflectionOperations is the right structural improvement; ConcurrentDictionary<ICustomAttributeProvider, Attribute[]> is thread-safe and handles parallel test execution correctly.
  • The NotCachedReflectionAccessor inner class cleanly separates cached and uncached access paths, and the static Attribute[] GetAttributes(...) local function avoids a Func<> allocation on every cache-miss.
  • GetTestPropertiesAsTraits's two-pass count + single allocation pattern is a genuine performance win.
  • ClearCache() is correctly scoped to test manipulation scenarios and doesn't touch shared state.

🧠 Reviewed by Expert Code Reviewer 🧠

🧠 Reviewed by Expert Code Reviewer 🧠

Comment thread src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs Outdated
Copilot AI review requested due to automatic review settings May 11, 2026 12:10
Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Workflow: Expert Code Reviewer 🧠
Date: 2026-05-11
Repository: microsoft/testfx


Key Findings

All three issues flagged in prior reviews have been correctly resolved in commit fae08031:

✅ Resolved — Unsafe (ReflectionOperations) casts removed

Both TestDeployment and DeploymentUtilityBase no-arg constructors now pass PlatformServiceProvider.Instance.ReflectionOperations directly to DeploymentItemUtility without a concrete-type cast. The InvalidCastException risk when using MockableReflectionOperations in tests is eliminated.

✅ Resolved — DeclaringType! null-forgiving operator replaced

IsMethodDeclaredInSameAssemblyAsType now correctly uses method.DeclaringType?.Assembly.Equals(type.Assembly) ?? false, guarding against global IL methods where DeclaringType is null by contract.

✅ Resolved — NotCachedReflectionAccessor hard-cast replaced

The (Assembly)attributeProvider cast is replaced with a well-structured switch expression that explicitly handles MemberInfo and Assembly cases, and throws a descriptive ArgumentException (which is caught and logged upstream) for unsupported providers like Module or ParameterInfo.

Positive Observations

  • The switch expression in GetCustomAttributesNotCached is clean and correctly handles all framework-significant cases; the _ => throw new ArgumentException(...) arm produces a descriptive diagnostic rather than a silent miscast.
  • The ?. null-conditional on DeclaringType with a ?? false fallback is exactly the right pattern.
  • No new correctness, threading, or API-compatibility concerns were introduced in this commit.

🧠 Reviewed by Expert Code Reviewer 🧠

🧠 Reviewed by Expert Code Reviewer 🧠

Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx
PR: #7344 — Unify reflection helpers (updated review, head fae0803)


What Was Fixed in This Commit ✅

This commit ("Address review comments") resolves the four issues flagged in the prior review:

  • All 12 SequenceEqual(...).Should().BeTrue() occurrences in ReflectionOperationsTests.cs → replaced with actual.Should().Equal(expected)
  • BeEquivalentTo in ReflectionUtilityTests.cs (integration) → replaced with Equal
  • Dead SetupDeploymentItems method in DesktopTestDeploymentTests.cs → removed ✅
  • MockableReflectionOperations.GetCustomAttributesCached → null-safe for the Assembly branch ✅
  • AssemblyEnumeratorTests.TestableAssemblyEnumerator → simplified to Mock<ReflectHelper> directly ✅

Remaining Issues

Two items from the earlier reviews are still open:

  1. [Assertion] ReflectionUtilityTests.cs:30 — One SequenceEqual(...).Should().BeTrue() was missed because it lives inside a #if NETFRAMEWORK block. (Inline comment posted.)

  2. [Assertion] AttributeMockingHelper.SetCustomAttribute — the Type type parameter is stored but never used during filtering in GetCustomAttributesNotCached. The actual type filtering happens upstream via .OfType<T>() in MockableReflectionOperations, so there's no false-positive today, but the API is misleading. (Inline comment posted.)

Two items from the first review (run 25663966959) remain open but are minor/style-level:

  1. Stale test method names HasAttributeDerivedFrom* (lines 272, 285, 298, 319 of ReflectionOperationsTests.cs) — these methods now call IsAttributeDefined<T> internally.
  2. Dead code comments // new Mock<MemberInfo>(); (lines 258, 305 of ReflectionOperationsTests.cs).

Overall Assessment

No flakiness or isolation bugs. The migration from ReflectHelper to IReflectionOperations is sound and well-tested. All remaining items are minor clean-up suggestions.


🧪 Test quality reviewed by Test Expert Reviewer 🧪

🧪 Test quality reviewed by Test Expert Reviewer 🧪

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 25/25 changed files
  • Comments generated: 0 new

@Evangelink
Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 11, 2026

@copilot resolve the merge conflicts in this pull request

Merge conflicts are resolved in commit 91b4074 by merging origin/main into this branch and resolving the conflict in src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs.

Copilot AI changed the title Unify reflection helpers &lt;empty&gt; May 11, 2026
@Evangelink Evangelink changed the title &lt;empty&gt; Unify reflection helpers May 11, 2026
Copilot AI review requested due to automatic review settings May 11, 2026 19:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Comments suppressed due to low confidence (4)

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs:255

  • This GetAttributes(...) setup targets the Moq mock, but TestablePlatformServiceProvider exposes a MockableReflectionOperations wrapper whose generic methods filter GetCustomAttributes results instead of delegating to the mock's generic setups. As a result this setup is unused and can be misleading; prefer ensuring GetCustomAttributes(methodInfo) returns the DataSourceAttribute you want the wrapper to surface.
        // Setup mocks
        _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(methodInfo)).Returns(attributes);
        _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetAttributes<DataSourceAttribute>(methodInfo)).Returns(attributes.OfType<DataSourceAttribute>());

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs:283

  • Same as above: setting up MockReflectionOperations.GetAttributes(...) has no effect when calls go through MockableReflectionOperations (it filters GetCustomAttributes instead). Consider removing this setup and relying on GetCustomAttributes(_methodInfo) to contain the DataSourceAttribute.

        // Setup mocks
        _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes);
        _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetAttributes<DataSourceAttribute>(_methodInfo)).Returns([dataSourceAttribute]);

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs:338

  • This GetCustomAttributesCached setup is on the Moq mock, but the code under test calls ReflectionOperations.GetCustomAttributesCached via MockableReflectionOperations, which does not delegate to the mock's GetCustomAttributesCached. The setup is therefore dead and can be removed to avoid confusion.

        // Setup mocks
        _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes);
        _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributesCached(_methodInfo)).Returns(attributes);

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs:365

  • Same issue here: MockReflectionOperations.GetCustomAttributesCached(...) is never invoked when using MockableReflectionOperations, so this setup is unused. Prefer configuring GetCustomAttributes(_methodInfo) instead (which the wrapper uses) or update the wrapper to respect cached setups.

        // Setup mocks
        _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes);
        _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributesCached(_methodInfo)).Returns(attributes);

  • Files reviewed: 26/26 changed files
  • Comments generated: 6

Comment on lines +185 to +199
IEnumerable<RetryBaseAttribute> attributes = PlatformServiceProvider.Instance.ReflectionOperations.GetAttributes<RetryBaseAttribute>(MethodInfo);
using IEnumerator<RetryBaseAttribute> enumerator = attributes.GetEnumerator();
if (!enumerator.MoveNext())
{
if (attribute is RetryBaseAttribute retryAttribute)
{
if (found is not null)
{
ThrowMultipleAttributesException(nameof(RetryBaseAttribute));
}
return null;
}

found = retryAttribute;
}
RetryBaseAttribute attribute = enumerator.Current;

if (enumerator.MoveNext())
{
ThrowMultipleAttributesException(nameof(RetryBaseAttribute));
}

return found;
return attribute;

// Setup mocks
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes);
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetAttributes<DataSourceAttribute>(_methodInfo)).Returns([dataSourceAttribute]);

// Setup mocks
_testablePlatformServiceProvider.MockReflectionOperations.Setup(ro => ro.GetCustomAttributes(_methodInfo)).Returns(attributes);
_testablePlatformServiceProvider.MockReflectionOperations.Setup(ro => ro.GetCustomAttributesCached(_methodInfo)).Returns(attributes);
Comment on lines +20 to +23
// PERF: This was moved from Dictionary<MemberInfo, Dictionary<string, object>> to Concurrent<ICustomAttributeProvider, Attribute[]>
// storing an array allows us to store multiple attributes of the same type if we find them. It also has lower memory footprint, and is faster
// when we are going through the whole collection. Giving us overall better perf.
private readonly ConcurrentDictionary<ICustomAttributeProvider, Attribute[]> _attributeCache = [];
/// </returns>
[SecurityCritical]
#if NET5_0_OR_GREATER
[Obsolete]
Comment on lines +15 to 16
internal sealed class ReflectionOperations : MarshalByRefObject, IReflectionOperations
{
Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx

Key Findings

  1. Dead mock setups in TestMethodRunnerTests — Six new mock setups (GetAttributes<DataSourceAttribute> ×3, GetCustomAttributesCached ×3) are never actually called. MockableReflectionOperations routes both methods through mock.GetCustomAttributes, not through the respective generic/cached mock methods. The tests pass correctly via the existing GetCustomAttributes setup, but the extra setups are misleading and create maintenance confusion.

  2. MockableReflectionOperations.GetCustomAttributesCached doesn't cache — While this is an acceptable tradeoff for a test double, the name implies once-per-member semantics. Without a comment documenting this intentional difference, future readers may assume the wrapper has caching behavior equivalent to the production ReflectionOperations.

  3. Missing coverage for GetSingleAttributeOrDefault and GetFirstAttributeOrDefault — Both new IReflectionOperations methods lack unit tests. In particular, GetSingleAttributeOrDefault's InvalidOperationException throw path and GetFirstAttributeOrDefault's sealed-type assertion are untested.

Positive Observations

  • The migration from SequenceEqual(...).Should().BeTrue() to actual.Should().Equal(expected) throughout the test suite is a meaningful improvement in assertion quality.
  • The MockableReflectionOperations design cleanly solves the Moq/generic-method-with-constraints limitation while keeping test setup intuitive.
  • The IsAttributeDefinedShouldReturnFromCache / HasAttributeDerivedFromShouldReturnFromCache tests correctly validate caching behavior via the PlatformServiceProvider.Instance indirection and are architecturally sound.
  • Integration tests properly migrated from ReflectionOnlyLoadFrom to Assembly.LoadFrom, removing the fragile ReflectionOnlyAssemblyResolve handler.

Recommendations

  • Remove the 6 redundant mock setups in TestMethodRunnerTests.
  • Add a clarifying comment to MockableReflectionOperations.GetCustomAttributesCached about the intentional absence of caching.
  • Add unit tests for GetSingleAttributeOrDefault (including the multi-attribute exception path) and GetFirstAttributeOrDefault.

Generated by Test Expert Reviewer 🧪

🧪 Test quality reviewed by Test Expert Reviewer 🧪


// Setup mocks
_testablePlatformServiceProvider.MockReflectionOperations.Setup(ro => ro.GetCustomAttributes(_methodInfo)).Returns(attributes);
_testablePlatformServiceProvider.MockReflectionOperations.Setup(ro => ro.GetCustomAttributesCached(_methodInfo)).Returns(attributes);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Structure] This mock setup for GetCustomAttributesCached is also dead code.

MockableReflectionOperations.GetCustomAttributesCached is implemented as:

public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider)
    => attributeProvider switch
    {
        MemberInfo memberInfo => mock.Object.GetCustomAttributes(memberInfo)?.OfType<Attribute>().ToArray() ?? [],
        ...
    };

It calls mock.Object.GetCustomAttributes(...), not mock.Object.GetCustomAttributesCached(...). So this setup is never matched.

Impact: Same as the GetAttributes<T> issue — the GetCustomAttributes setup on the previous line is the one actually driving behavior. The GetCustomAttributesCached setup creates a false sense of test coverage and could mislead future maintainers.

Suggestion: Remove this setup. The same dead-setup pattern appears at lines 337 and 364.


// Setup mocks
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes);
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetAttributes<DataSourceAttribute>(_methodInfo)).Returns([dataSourceAttribute]);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Structure] This mock setup for GetAttributes<DataSourceAttribute> is dead code and will never be called.

MockableReflectionOperations.GetAttributes<TAttributeType> is implemented as:

=> GetCustomAttributesCached(attributeProvider).OfType<TAttributeType>();

It calls this.GetCustomAttributesCachedmock.Object.GetCustomAttributes(memberInfo). It does not call mock.Object.GetAttributes<T>(...). So this Setup on MockReflectionOperations is never invoked.

Impact: The test appears to control behavior through this setup, but the actual filtering happens from the GetCustomAttributes mock (already set up on the line above). This is misleading to readers and creates maintenance confusion — if someone relies solely on this setup assuming it controls behavior, the test will appear to pass while the actual mock call is something different.

Suggestion: Remove this setup. The GetCustomAttributes setup two lines above is sufficient — MockableReflectionOperations will automatically filter it to DataSourceAttribute instances when GetAttributes<DataSourceAttribute> is called. The same applies to lines 254 and 282.

where TAttributeType : Attribute
=> GetCustomAttributesCached(attributeProvider).OfType<TAttributeType>();

public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Structure] GetCustomAttributesCached on MockableReflectionOperations doesn't actually cache — it re-queries mock.Object.GetCustomAttributes on every call.

The name "Cached" implies once-per-member semantics, but the implementation:

public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider)
    => attributeProvider switch
    {
        MemberInfo memberInfo => mock.Object.GetCustomAttributes(memberInfo)?.OfType<Attribute>().ToArray() ?? [],
        ...
    };

...hits the mock on every invocation.

Impact: Any production-code path that relies on caching to avoid repeated attribute lookups won't be tested correctly when using this test double. A test that calls a method which internally invokes GetCustomAttributesCached ten times would still pass, while production ReflectionOperations would only call the underlying reflection API once. This gap means tests won't catch bugs where caching is accidentally removed.

Suggestion: This is a known tradeoff in a test double. Consider adding a // Note: no caching by design — each call re-queries the mock comment to document the intentional difference from production, so future readers don't assume caching semantics.

@@ -12,8 +18,33 @@ namespace MSTestAdapter.PlatformServices.UnitTests.Services;
public class ReflectionOperationsTests : TestContainer
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Coverage] The new GetSingleAttributeOrDefault<T> and GetFirstAttributeOrDefault<T> methods on ReflectionOperations have no unit tests.

These are non-trivial methods:

  • GetFirstAttributeOrDefault has a DebugEx.Assert(typeof(TAttribute).IsSealed) guard — no test verifies what happens with non-sealed types in debug builds.
  • GetSingleAttributeOrDefault throws InvalidOperationException when multiple attributes are found — this error path is entirely untested.

Suggestion: Add at least:

  1. A happy-path test for each method (attribute found, attribute not found).
  2. A test that GetSingleAttributeOrDefault throws when two matching attributes are present.
  3. Optionally, a test that GetFirstAttributeOrDefault only returns the first match when multiple are present.

Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Workflow: Expert Code Reviewer 🧠
Date: 2026-05-11
Repository: microsoft/testfx

Key Findings

Performance Regression — GetRetryAttribute allocates an iterator state machine (Medium)

TestMethodInfo.GetRetryAttribute() now calls GetAttributes<RetryBaseAttribute>(MethodInfo), which is implemented as a yield return iterator. The old code explicitly avoided this allocation with a comment: "Iterate the cached attribute array directly to avoid allocating the yield-return state machine." Since GetRetryAttribute is called from the TestMethodInfo constructor (once per executed test method), this reintroduces measurable GC pressure at scale. See inline comment.

Defensive Coding Gap — NotCachedReflectionAccessor NullReferenceException + undocumented contract (Low-Medium)

The _ => arm in GetCustomAttributesNotCached evaluates attributeProvider.GetType() in the exception message before guarding against null, so a null input surfaces as NullReferenceException rather than a clean ArgumentNullException. Additionally, valid ICustomAttributeProvider subtypes (ParameterInfo, Module) throw ArgumentException at runtime without the interface contract documenting this constraint. See inline comment.

Positive Observations

  • The consolidation of the attribute cache from ReflectHelper (singleton) into ReflectionOperations is architecturally sound — ConcurrentDictionary<ICustomAttributeProvider, Attribute[]> is the right data structure for thread-safe, low-memory-footprint attribute caching.
  • Deletion of ReflectionOperationsNetFrameworkAttributeHelpers is justified: AssemblyLoadWorker uses CustomAttributeData directly for reflection-only assemblies, so there are no product code paths that require the old reflection-only-load workaround.
  • GetRetryAttribute's two-step enumerator pattern (get first, check for second) correctly preserves the single-attribute invariant and properly disposes the enumerator via using.
  • MockableReflectionOperations is a clean solution to the Moq generic-method-with-constraint limitation.
  • GetTestPropertiesAsTraits's two-pass (count then fill) approach correctly avoids a List<T> allocation for the common zero-traits case.
  • InitializeLifetimeService is correctly guarded with #if NET5_0_OR_GREATER [Obsolete] to suppress the BCL's obsolete warning while preserving the infinite-lifetime behavior on .NET Framework.

Recommendations

  1. Restore the direct cache-array iteration in GetRetryAttribute to avoid the state-machine allocation.
  2. Add a null guard and a clear exception message in NotCachedReflectionAccessor.GetCustomAttributesNotCached.

Generated by Expert Code Reviewer 🧠

🧠 Reviewed by Expert Code Reviewer 🧠

@@ -183,22 +182,20 @@ private TestMethodAttribute GetTestMethodAttribute()
/// </returns>
private RetryBaseAttribute? GetRetryAttribute()
{
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Performance] This reverses a deliberate per-test-method allocation optimization.

The old GetRetryAttribute had an explicit comment: "Iterate the cached attribute array directly to avoid allocating the yield-return state machine that GetAttributes<T>() would create." The new code calls GetAttributes<RetryBaseAttribute>(MethodInfo), which is backed by a yield return iterator in ReflectionOperations, so it allocates an IEnumerator<RetryBaseAttribute> state-machine object on every call. GetRetryAttribute is called from TestMethodInfo's constructor — i.e., once per test method during execution.

Impact: Small but measurable extra GC pressure proportional to the number of tests. The old optimization path was intentional.

Suggestion: Either iterate the cached array directly (as the old code did) or add an overload to GetCustomAttributesCached / GetAttributes that avoids the state machine — e.g., accepting a callback/delegate pattern or providing a Span<T>-returning variant. The simplest fix consistent with the refactoring goals:

// Iterate directly from the cache, avoiding iterator state machine allocation.
RetryBaseAttribute? found = null;
foreach (Attribute attribute in PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(MethodInfo))
{
    if (attribute is RetryBaseAttribute retryAttribute)
    {
        if (found is not null) ThrowMultipleAttributesException(nameof(RetryBaseAttribute));
        found = retryAttribute;
    }
}
return found;

{
IReflectionOperations reflectionOperations = PlatformServiceProvider.Instance.ReflectionOperations;
object[] attributesArray = attributeProvider switch
{
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Defensive] Two problems in the _ => default arm of the switch:

  1. NullReferenceException on null input: If attributeProvider is null it reaches the _ arm, but then attributeProvider.GetType() in the interpolated string throws NullReferenceException before the ArgumentException is constructed. The caller sees NRE instead of a meaningful ArgumentNullException.

  2. Undocumented contract restriction: IReflectionOperations.GetCustomAttributesCached accepts ICustomAttributeProvider, which includes ParameterInfo and Module — both valid ICustomAttributeProvider implementations. Passing either throws ArgumentException at runtime without being documented in the interface XML doc or enforced by the type system.

Impact: Callers of the internal interface who (reasonably) pass a ParameterInfo (e.g., for parameter-level attributes) get an unexpected runtime exception rather than a compile-time error. Null input silently escalates to NRE.

Suggestion:

if (attributeProvider is null)
    throw new ArgumentNullException(nameof(attributeProvider));
// ...switch...
_ => throw new ArgumentException(
    $"Unsupported attribute provider type: {attributeProvider.GetType()}.  " +
    "Only MemberInfo and Assembly are supported.", nameof(attributeProvider)),

Also consider adding /// <exception cref="ArgumentException">Thrown for unsupported provider types.</exception> to IReflectionOperations.GetCustomAttributesCached.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants